Skip to content

Method: withParameter(String, Value)

1: /*
2: * *********************************************************************************************************************
3: *
4: * blueMarine II: Semantic Media Centre
5: * http://tidalwave.it/projects/bluemarine2
6: *
7: * Copyright (C) 2015 - 2021 by Tidalwave s.a.s. (http://tidalwave.it)
8: *
9: * *********************************************************************************************************************
10: *
11: * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
12: * the License. You may obtain a copy of the License at
13: *
14: * http://www.apache.org/licenses/LICENSE-2.0
15: *
16: * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
17: * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
18: * specific language governing permissions and limitations under the License.
19: *
20: * *********************************************************************************************************************
21: *
22: * git clone https://bitbucket.org/tidalwave/bluemarine2-src
23: * git clone https://github.com/tidalwave-it/bluemarine2-src
24: *
25: * *********************************************************************************************************************
26: */
27: package it.tidalwave.bluemarine2.model.impl.catalog.finder;
28:
29: import javax.annotation.Nonnegative;
30: import javax.annotation.Nonnull;
31: import javax.inject.Inject;
32: import java.util.ArrayList;
33: import java.util.Arrays;
34: import java.util.List;
35: import java.util.Optional;
36: import java.util.concurrent.atomic.AtomicInteger;
37: import java.util.function.Function;
38: import java.util.regex.Matcher;
39: import java.util.regex.Pattern;
40: import java.util.stream.Stream;
41: import java.io.IOException;
42: import java.io.InputStream;
43: import org.springframework.beans.factory.annotation.Configurable;
44: import org.springframework.util.StreamUtils;
45: import org.eclipse.rdf4j.model.IRI;
46: import org.eclipse.rdf4j.model.Value;
47: import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
48: import org.eclipse.rdf4j.query.QueryLanguage;
49: import org.eclipse.rdf4j.query.TupleQuery;
50: import org.eclipse.rdf4j.query.TupleQueryResult;
51: import org.eclipse.rdf4j.repository.Repository;
52: import org.eclipse.rdf4j.repository.RepositoryConnection;
53: import it.tidalwave.util.Finder;
54: import it.tidalwave.util.Id;
55: import it.tidalwave.util.LoggingUtilities;
56: import it.tidalwave.util.ReflectionUtils;
57: import it.tidalwave.util.Task;
58: import it.tidalwave.util.spi.FinderSupport;
59: import it.tidalwave.role.ContextManager;
60: import it.tidalwave.bluemarine2.util.ImmutableTupleQueryResult;
61: import it.tidalwave.bluemarine2.model.impl.catalog.factory.RepositoryEntityFactory;
62: import it.tidalwave.bluemarine2.model.spi.CacheManager;
63: import it.tidalwave.bluemarine2.model.spi.CacheManager.Cache;
64: import it.tidalwave.bluemarine2.model.spi.SourceAwareFinder;
65: import lombok.EqualsAndHashCode;
66: import lombok.Getter;
67: import lombok.RequiredArgsConstructor;
68: import lombok.Setter;
69: import lombok.ToString;
70: import lombok.extern.slf4j.Slf4j;
71: import static java.util.stream.Collectors.*;
72: import static java.nio.charset.StandardCharsets.UTF_8;
73: import static it.tidalwave.bluemarine2.util.RdfUtilities.streamOf;
74: import static it.tidalwave.bluemarine2.model.vocabulary.BMMO.*;
75:
76: /***********************************************************************************************************************
77: *
78: * A base class for creating {@link Finder}s.
79: *
80: * @param <ENTITY> the entity the {@code Finder} should find
81: * @param <FINDER> the subclass
82: *
83: * @stereotype Finder
84: *
85: * @author Fabrizio Giudici
86: *
87: **********************************************************************************************************************/
88: @Configurable @Slf4j
89: public class RepositoryFinderSupport<ENTITY, FINDER extends Finder<ENTITY>>
90: extends FinderSupport<ENTITY, FINDER>
91: implements SourceAwareFinder<ENTITY, FINDER>
92: {
93: private static final String REGEX_BINDING_TAG = "^@([A-Za-z0-9]*)@";
94:
95: private static final String REGEX_BINDING_TAG_LINE = REGEX_BINDING_TAG + ".*$";
96:
97: private static final String REGEX_COMMENT = "^ *#.*";
98:
99: private static final String PREFIXES = "PREFIX foaf: <http://xmlns.com/foaf/0.1/>\n"
100: + "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>\n"
101: + "PREFIX rel: <http://purl.org/vocab/relationship/>\n"
102: + "PREFIX bmmo: <http://bluemarine.tidalwave.it/2015/04/mo/>\n"
103: + "PREFIX mo: <http://purl.org/ontology/mo/>\n"
104: + "PREFIX vocab: <http://dbtune.org/musicbrainz/resource/vocab/>\n"
105: + "PREFIX xs: <http://www.w3.org/2001/XMLSchema#>\n";
106:
107: private static final String QUERY_COUNT_HOLDER = "queryCount";
108:
109: private static final long serialVersionUID = 1896412264314804227L;
110:
111: private static final SimpleValueFactory FACTORY = SimpleValueFactory.getInstance();
112:
113: @Nonnull
114: protected final transient Repository repository;
115:
116: @Nonnull
117: private final Class<ENTITY> entityClass;
118:
119: @Nonnull
120: private final String idName;
121:
122: @Nonnull
123: private final transient Optional<Id> id;
124:
125: @Nonnull
126: private final transient Optional<Value> source;
127:
128: @Nonnull
129: private final transient Optional<Value> sourceFallback;
130:
131: @Inject
132: private transient ContextManager contextManager;
133:
134: @Inject
135: private transient RepositoryEntityFactory entityFactory;
136:
137: @Inject
138: private transient CacheManager cacheManager;
139:
140: // FIXME: move to a stats bean
141: private static final AtomicInteger queryCount = new AtomicInteger();
142:
143: @Getter @Setter
144: private static boolean dumpThreadOnQuery = false;
145:
146: /*******************************************************************************************************************
147: *
148: *
149: *
150: ******************************************************************************************************************/
151: @RequiredArgsConstructor(staticName = "withSparql") @EqualsAndHashCode @ToString
152: protected static class QueryAndParameters
153: {
154: @Getter @Nonnull
155: private final String sparql;
156:
157: @Nonnull
158: private final List<Object> parameters = new ArrayList<>();
159:
160: @Nonnull
161: public QueryAndParameters withParameter (@Nonnull final String name, @Nonnull final Optional<? extends Value> value)
162: {
163: return value.map(v -> withParameter(name, v)).orElse(this);
164: }
165:
166: @Nonnull
167: public QueryAndParameters withParameter (@Nonnull final String name, @Nonnull final Value value)
168: {
169: parameters.addAll(List.of(name, value));
170: return this;
171: }
172:
173: @Nonnull
174: public Object[] getParameters()
175: {
176: return parameters.toArray();
177: }
178:
179: @Nonnull
180: private String getCountSparql()
181: {
182: return String.format("SELECT (COUNT(*) AS ?%s)%n {%n%s%n }",
183: QUERY_COUNT_HOLDER,
184: sparql.replaceAll("ORDER BY[\\s\\S]*", ""));
185: }
186: }
187:
188: /*******************************************************************************************************************
189: *
190: *
191: *
192: ******************************************************************************************************************/
193: protected RepositoryFinderSupport (@Nonnull final Repository repository, @Nonnull final String idName)
194: {
195: this.repository = repository;
196: this.entityClass = (Class<ENTITY>)ReflectionUtils.getTypeArguments(RepositoryFinderSupport.class, getClass()).get(0);
197: this.idName = idName;
198: this.id = Optional.empty();
199: this.source = Optional.of(O_SOURCE_EMBEDDED); // FIXME: resets
200: this.sourceFallback = Optional.empty(); // FIXME: resets
201: }
202:
203: /*******************************************************************************************************************
204: *
205: *
206: *
207: ******************************************************************************************************************/
208: private RepositoryFinderSupport (@Nonnull final Repository repository,
209: @Nonnull final Class<ENTITY> entityClass,
210: @Nonnull final String idName,
211: @Nonnull final Optional<Id> id,
212: @Nonnull final Optional<Value> source,
213: @Nonnull final Optional<Value> sourceFallback)
214: {
215: this.repository = repository;
216: this.entityClass = entityClass;
217: this.idName = idName;
218: this.id = id;
219: this.source = source;
220: this.sourceFallback = sourceFallback;
221: }
222:
223: /*******************************************************************************************************************
224: *
225: * Clone constructor.
226: *
227: ******************************************************************************************************************/
228: public RepositoryFinderSupport (@Nonnull final RepositoryFinderSupport<ENTITY, FINDER> other,
229: @Nonnull final Object override)
230: {
231: super(other, override);
232: final RepositoryFinderSupport<ENTITY, FINDER> source = getSource(RepositoryFinderSupport.class, other, override);
233: this.repository = source.repository;
234: this.entityClass = source.entityClass;
235: this.idName = source.idName;
236: this.id = source.id;
237: this.source = source.source;
238: this.sourceFallback = source.sourceFallback;
239: }
240:
241: /*******************************************************************************************************************
242: *
243: * {@inheritDoc}
244: *
245: ******************************************************************************************************************/
246: @Override @Nonnull
247: protected final List<? extends ENTITY> computeNeededResults()
248: {
249: return query(QueryAndParameters::getSparql,
250: result -> createEntities(repository, entityClass, result),
251: result -> String.format("%d entities", result.size()));
252: }
253:
254: /*******************************************************************************************************************
255: *
256: * {@inheritDoc}
257: *
258: ******************************************************************************************************************/
259: @Override @Nonnegative
260: public int count()
261: {
262: return query(QueryAndParameters::getCountSparql,
263: result -> Integer.parseInt(result.next().getValue(QUERY_COUNT_HOLDER).stringValue()),
264: result -> String.format("%d", result));
265: }
266:
267: /*******************************************************************************************************************
268: *
269: * {@inheritDoc}
270: *
271: ******************************************************************************************************************/
272: @Override @Nonnull
273: public FINDER withId (@Nonnull final Id id)
274: {
275: return clonedWith(new RepositoryFinderSupport(repository,
276: entityClass,
277: idName,
278: Optional.of(id),
279: source,
280: sourceFallback));
281: }
282:
283: /*******************************************************************************************************************
284: *
285: * {@inheritDoc}
286: *
287: ******************************************************************************************************************/
288: @Override @Nonnull
289: public FINDER importedFrom (@Nonnull final Optional<Id> optionalSource)
290: {
291: return optionalSource.map(this::importedFrom).orElse((FINDER)this);
292: }
293:
294: /*******************************************************************************************************************
295: *
296: * {@inheritDoc}
297: *
298: ******************************************************************************************************************/
299: @Override @Nonnull
300: public FINDER importedFrom (@Nonnull final Id source)
301: {
302: return clonedWith(new RepositoryFinderSupport(repository,
303: entityClass,
304: idName,
305: id,
306: Optional.of(FACTORY.createLiteral(source.toString())),
307: sourceFallback));
308: }
309:
310: /*******************************************************************************************************************
311: *
312: * {@inheritDoc}
313: *
314: ******************************************************************************************************************/
315: @Override @Nonnull
316: public FINDER withFallback (@Nonnull final Optional<Id> sourceFallback)
317: {
318: return sourceFallback.map(this::withFallback).orElse((FINDER)this);
319: }
320:
321: /*******************************************************************************************************************
322: *
323: * {@inheritDoc}
324: *
325: ******************************************************************************************************************/
326: @Override @Nonnull
327: public FINDER withFallback (@Nonnull final Id sourceFallback)
328: {
329: return clonedWith(new RepositoryFinderSupport(repository,
330: entityClass,
331: idName,
332: id,
333: source,
334: Optional.of(FACTORY.createLiteral(sourceFallback.toString()))));
335: }
336:
337: /*******************************************************************************************************************
338: *
339: * Returns the count of queries performed so far.
340: *
341: * @return the count of queries
342: *
343: ******************************************************************************************************************/
344: @Nonnegative
345: public static int getQueryCount()
346: {
347: return queryCount.intValue();
348: }
349:
350: /*******************************************************************************************************************
351: *
352: * Resets the count of queries performed so far.
353: *
354: ******************************************************************************************************************/
355: public static void resetQueryCount()
356: {
357: queryCount.set(0);
358: }
359:
360: /*******************************************************************************************************************
361: *
362: * Prepares the SPARQL query and its parameters.
363: *
364: * @return the SPARQL query and its parameters
365: *
366: ******************************************************************************************************************/
367: @Nonnull
368: protected /* abstract */ QueryAndParameters prepareQuery()
369: {
370: throw new UnsupportedOperationException("Must be implemented by subclasses");
371: }
372:
373: /*******************************************************************************************************************
374: *
375: * Performs a query, eventually using the cache.
376: *
377: * @param sparqlSelector a function that select the SPARQL statement to use
378: * @param finalizer a function to transform the query raw result into the final result
379: * @param resultToString a function that provide the logging string for the result
380: * @return the found entities
381: *
382: ******************************************************************************************************************/
383: @Nonnull
384: private <E> E query (@Nonnull final Function<QueryAndParameters, String> sparqlSelector,
385: @Nonnull final Function<TupleQueryResult, E> finalizer,
386: @Nonnull final Function<E, String> resultToString)
387: {
388: log.info("query() - {}", entityClass);
389: final long baseTime = System.nanoTime();
390: final QueryAndParameters queryAndParameters = prepareQuery()
391: .withParameter(idName, id.map(this::iriFor))
392: .withParameter("source", source)
393: .withParameter("fallback", sourceFallback.equals(source) ? Optional.empty() : sourceFallback);
394: final Object[] parameters = queryAndParameters.getParameters();
395: final String originalSparql = sparqlSelector.apply(queryAndParameters);
396: final String sparql = PREFIXES + Stream.of(originalSparql.split("\n"))
397: .filter(s -> matchesTag(s, parameters))
398: .map(s -> s.replaceAll(REGEX_BINDING_TAG, ""))
399: .collect(joining("\n"));
400: log(originalSparql, sparql, parameters);
401: final E result = query(sparql, finalizer, parameters);
402: queryCount.incrementAndGet();
403: final long elapsedTime = System.nanoTime() - baseTime;
404: log.info(">>>> query returned {} in {} msec", resultToString.apply(result), elapsedTime / 1E6);
405: LoggingUtilities.dumpStack(this, dumpThreadOnQuery);
406:
407: return result;
408: }
409:
410: /*******************************************************************************************************************
411: *
412: * Performs a query.
413: *
414: * @param sparql the SPARQL of the query
415: * @param finalizer a function to transform the query raw result into the final result
416: * @param parameters an optional set of parameters of the query ("name", value, "name", value ,,,)
417: * @return the found entities
418: *
419: ******************************************************************************************************************/
420: @Nonnull
421: private <R> R query (@Nonnull final String sparql,
422: @Nonnull final Function<TupleQueryResult, R> finalizer,
423: @Nonnull final Object ... parameters)
424: {
425: try (final RepositoryConnection connection = repository.getConnection())
426: {
427: final TupleQuery query = connection.prepareTupleQuery(QueryLanguage.SPARQL, sparql);
428:
429: for (int i = 0; i < parameters.length; i += 2)
430: {
431: query.setBinding((String)parameters[i], (Value)parameters[i + 1]);
432: }
433: //
434: // Don't cache entities because they are injected with DCI roles in function of the context.
435: // Caching tuples is safe.
436: final Cache cache = cacheManager.getCache(RepositoryFinderSupport.class);
437: final String key = String.format("%s # %s", compacted(sparql), Arrays.toString(parameters));
438:
439: try (final ImmutableTupleQueryResult result = cache.getCachedObject(key,
440: () -> new ImmutableTupleQueryResult(query.evaluate())))
441: {
442: // ImmutableTupleQueryResult is not thread safe, so clone the cached instance
443: return finalizer.apply(new ImmutableTupleQueryResult(result));
444: }
445: }
446: }
447:
448: /*******************************************************************************************************************
449: *
450: * Facility method that creates an {@link IRI} given an {@link Id}.
451: *
452: * @param id the {@code Id}
453: * @return the {@code IRI}
454: *
455: ******************************************************************************************************************/
456: @Nonnull
457: protected Value iriFor (@Nonnull final Id id)
458: {
459: return FACTORY.createIRI(id.stringValue());
460: }
461:
462: /*******************************************************************************************************************
463: *
464: *
465: ******************************************************************************************************************/
466: @Nonnull
467: protected Value literalFor (final boolean b)
468: {
469: return FACTORY.createLiteral(b);
470: }
471:
472: /*******************************************************************************************************************
473: *
474: * Reads a SPARQL statement from a named resource
475: *
476: * @param clazz the reference class
477: * @param name the resource name
478: * @return the SPARQL statement
479: *
480: ******************************************************************************************************************/
481: @Nonnull
482: protected static String readSparql (@Nonnull final Class<?> clazz, @Nonnull final String name)
483: {
484: try (final InputStream is = clazz.getResourceAsStream(name))
485: {
486: return Stream.of(StreamUtils.copyToString(is, UTF_8)
487: .split("\n"))
488: .filter(s -> !s.matches(REGEX_COMMENT))
489: .collect(joining("\n"));
490: }
491: catch (IOException e)
492: {
493: throw new RuntimeException(e);
494: }
495: }
496:
497: /*******************************************************************************************************************
498: *
499: * Instantiates an entity for each given {@link TupleQueryResult}. Entities are instantiated in the DCI contents
500: * associated to this {@link Finder} - see {@link #getContexts()}.
501: *
502: * @param <E> the static type of the entities to instantiate
503: * @param repository the repository we're querying
504: * @param entityClass the dynamic type of the entities to instantiate
505: * #param queryResult the {@code TupleQueryResult}
506: * @return the instantiated entities
507: *
508: ******************************************************************************************************************/
509: @Nonnull
510: private <E> List<E> createEntities (@Nonnull final Repository repository,
511: @Nonnull final Class<E> entityClass,
512: @Nonnull final TupleQueryResult queryResult)
513: {
514: return contextManager.runWithContexts(getContexts(), new Task<>()
515: {
516: @Override @Nonnull
517: public List<E> run()
518: {
519: return streamOf(queryResult)
520: .map(bindingSet -> entityFactory.createEntity(repository, entityClass, bindingSet))
521: .collect(toList());
522: }
523: });
524: // TODO: requires TheseFoolishThings 3.1-ALPHA-3
525: // return contextManager.runWithContexts(getContexts(), () -> streamOf(queryResult)
526: // .map(bindingSet -> entityFactory.createEntity(repository, entityClass, bindingSet))
527: // .collect(toList()));
528: }
529:
530: /*******************************************************************************************************************
531: *
532: * Returns {@code true} if the given string contains a binding tag (in the form {@code @TAG@}) that matches one
533: * of the bindings; or if there are no binding tags. This is used as a filter to eliminate portions of SPARQL
534: * queries that don't match any binding.
535: *
536: * @param string the string
537: * @param bindings the bindings
538: * @return {@code true} if there is a match
539: *
540: ******************************************************************************************************************/
541: private static boolean matchesTag (@Nonnull final String string, @Nonnull final Object[] bindings)
542: {
543: final Pattern patternBindingTagLine = Pattern.compile(REGEX_BINDING_TAG_LINE);
544: final Matcher matcher = patternBindingTagLine.matcher(string);
545:
546: if (!matcher.matches())
547: {
548: return true;
549: }
550:
551: final String tag = matcher.group(1);
552:
553: for (int i = 0; i < bindings.length; i+= 2)
554: {
555: if (tag.equals(bindings[i]))
556: {
557: return true;
558: }
559: }
560:
561: return false;
562: }
563:
564: /*******************************************************************************************************************
565: *
566: * Logs the query at various detail levels.
567: *
568: * @param originalSparql the original SPARQL statement
569: * @param sparql the SPARQL statement after binding tag filtering
570: * @param bindings the bindings
571: *
572: ******************************************************************************************************************/
573: private void log (@Nonnull final String originalSparql,
574: @Nonnull final String sparql,
575: @Nonnull final Object ... bindings)
576: {
577: if (log.isTraceEnabled())
578: {
579: Stream.of(originalSparql.split("\n")).forEach(s -> log.trace(">>>> original query: {}", s));
580: }
581:
582: if (log.isDebugEnabled())
583: {
584: Stream.of(sparql.split("\n")).forEach(s -> log.debug(">>>> query: {}", s));
585: }
586:
587: if (!log.isDebugEnabled() && log.isInfoEnabled())
588: {
589: log.info(">>>> query: {}", compacted(sparql));
590: }
591:
592: if (log.isInfoEnabled())
593: {
594: log.info(">>>> query parameters: {}", Arrays.toString(bindings));
595: }
596: }
597:
598: /*******************************************************************************************************************
599: *
600: *
601: ******************************************************************************************************************/
602: @Nonnull
603: private static String compacted (@Nonnull final String sparql)
604: {
605: return sparql.replace("\n", " ").replaceAll("\\s+", " ").trim();
606: }
607: }